package cnsatmsmart.automation.mmi.view.layers;

import cnsatmsmart.CNSATMSMARTApp;
import cnsatmsmart.CNSATMSMARTView;
import com.bbn.openmap.LatLonPoint;
import com.bbn.openmap.Layer;
import com.bbn.openmap.MapBean;
import com.bbn.openmap.event.MapMouseEvent;
import com.bbn.openmap.event.MapMouseListener;
import com.bbn.openmap.event.ProjectionEvent;
import com.bbn.openmap.event.SelectMouseMode;
import com.bbn.openmap.omGraphics.OMArrowHead;
import com.bbn.openmap.omGraphics.OMGraphicConstants;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMLine;
import com.bbn.openmap.omGraphics.OMPoint;
import com.bbn.openmap.omGraphics.OMPoly;
import com.bbn.openmap.omGraphics.OMRect;
import com.bbn.openmap.omGraphics.OMText;
import com.bbn.openmap.proj.Projection;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JFileChooser;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

import cnsatmsmart.automation.mmi.controller.Buffer.Flight;
import cnsatmsmart.automation.mmi.controller.Buffer.FlightPlanList;
import cnsatmsmart.automation.mmi.controller.IO.ImageDriver;
import cnsatmsmart.automation.mmi.helper.utilities.TSafe.GlobalEnvVars;
import cnsatmsmart.automation.mmi.model.data.FlightPlan;
import cnsatmsmart.automation.mmi.model.data.TSafe.Fix;
import cnsatmsmart.automation.mmi.model.layers.OMLabelCircle;
import cnsatmsmart.automation.mmi.model.data.component.PNGFilter;

/**
 * @author SIK - PTIK, BPPT
 **/

public class FlightLayer extends Layer implements MapMouseListener {
    /* const */
        private static final int WHITE = 0;
        private static final int BLACK = 1;
        private static final int RED = 2;
        private static final int ORANGE = 3;
        private static final int YELLOW = 4;
        private static final int GRAY = 5;
        
        /**
         * mode tampilan penggambaran jalur tempuh pesawat : TIDAK DIGAMBARKAN
         */
        public static final int NO_TRAIL = -1;
        /**
         * mode tampilan penggambaran jalur tempuh pesawat : HANYA PESAWAT YANG DIPILIH
         */
        public static final int SELECTED_TRAIL = 0;
        /**
         * mode tampilan penggambaran jalur tempuh pesawat : SEMUA PESAWAT
         */
        public static final int ALL_TRAIL = 1;

        /**
         * mode tampilan penggambaran flightplan : TIDAK DIGAMBARKAN
         */
        public static final int NO_FLIGHTPLAN = -1;
        /**
         * mode tampilan penggambaran flightplan : HANYA PESAWAT YANG DIPILIH
         */
        public static final int SELECTED_FLIGHTPLAN = 0;
        /**
         * mode tampilan penggambaran flightplan : SEMUA PESAWAT
         */
        public static final int ALL_FLIGHTPLAN = 1;

        //colors of flight
            private static final Color[] COLORS = {Color.WHITE, Color.BLACK, Color.RED, Color.ORANGE, Color.YELLOW, Color.LIGHT_GRAY};

            /**
             * jari-jari default untuk penggambaran pesawat
             */
            public static final int FLIGHT_RADIUS = 10;        //nilai default untuk jari-jari penerbangan

    /* atribut - atribut yang dimiliki oleh kelas FlightLayer */
        private List flights = null;
        private Map < String, LatLonPoint > labels = new HashMap < String, LatLonPoint >();
        private HashMap < String, ArrayList < LatLonPoint > > trails = new HashMap < String, ArrayList < LatLonPoint > >();
        private Map < String, Long > trailed = new HashMap < String, Long >();
        private FlightPlanList FlightPlanList;
        
        private OMGraphicList graphics = new OMGraphicList();
        private Projection projection = null;
        private MapBean map;

        private int trailMode = NO_TRAIL;
        private int flightplanMode = NO_FLIGHTPLAN;

        private boolean calculateDistance = false;
        String f1 = null;
        String f2 = null;
        
        private boolean BearingDistance = false;

        String selectedFlightCS = null;
        String detailedFlightCS = null;

        MouseEvent mouseClicked = null;

        final InsetLayer layer = new InsetLayer();

        boolean showChart = false;
        boolean isMap = true;

        private JPanel panel = null;

        private JPopupMenu PopUpMenu = new JPopupMenu();    //pop up menu on right click mouse event
        private String ScreenTitle;                         //title of main screen

    /* constructor - user defined ctor, no default ctor */
        // <editor-fold defaultstate="collapsed" desc="ctor">
        /**
         * menginstansiasi object FlightLayer tanpa panel untuk meletakan elevation view
         * @param map MapBean
         * @param screentitle String yang merupakan judul dari layar
         * @param FlightPlanList FlightPlanList
         */
        public FlightLayer(MapBean map, String screentitle, FlightPlanList FlightPlanList) {
            this.ScreenTitle = screentitle;

            /*
             * pop up menu on right click mouse event
             */
            //<editor-fold defaultstate="colapsed" desc="Pop Up Menu">
            JMenuItem item1 = new JMenuItem("copy to clipboard");
            item1.addActionListener(
                new ActionListener() {
                    public void actionPerformed (ActionEvent e) {
                        ImageDriver jpegsv = new ImageDriver(ScreenTitle, 200);
                        Thread threadJPEG = new Thread(jpegsv);
                        threadJPEG.start();
                    }
                }
            );
            this.PopUpMenu.add(item1);
            
            JMenuItem item2 = new JMenuItem("save as PNG");
            item2.addActionListener(
                new ActionListener() {
                    public void actionPerformed (ActionEvent e) {
                        CNSATMSMARTView view = (CNSATMSMARTView)CNSATMSMARTApp.getApplication().getMainView();
                        view.CloseTopDialog();
                        
                        JFileChooser fc = new JFileChooser();
                        fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
                        fc.setAcceptAllFileFilterUsed(false);
                        fc.addChoosableFileFilter(new PNGFilter());
                        if (fc.showSaveDialog(null) == javax.swing.JFileChooser.APPROVE_OPTION) {
                            view.ShowTopDialog();
                            File file = fc.getSelectedFile();
                            String extension = PNGFilter.getExtension(file);
                            String filename = file.getAbsolutePath();
                            if ((extension == null) || (! extension.toLowerCase().equals(PNGFilter.PNG))) {
                                //selected file has no or wrong extension
                                    filename += "." + PNGFilter.PNG;
                                    file = new File(filename);
                            }
                            ImageDriver PNGsv = new ImageDriver(ScreenTitle, file, 200);
                            Thread threadPNG = new Thread(PNGsv);
                            threadPNG.start();
                        }
                    }
                }
            );
            this.PopUpMenu.add(item2);
            //</editor-fold>

            this.map = map;
            this.FlightPlanList = FlightPlanList;
        }
        /**
         * menginstansiasi object FlightLayer
         * @param map MapBean
         * @param panel JPanel yang merupakan panel untuk meletakan elevation view
         * @param screentitle String yang merupakan judul dari layar
         * @param FlightPlanList FlightPlanList
         */
        public FlightLayer(MapBean map, JPanel panel, String screentitle, FlightPlanList FlightPlanList) {
            this(map, screentitle, FlightPlanList);
            this.panel = panel;
        }
        // </editor-fold>

    /* setter */
        /**
         * mengupdate tampilan pada FlightLayer sesuai dengan data flight yang diterima
         * @param flights List yang merupakan data dari flights yang akan ditampilkan
         * @param isMap boolean : bernilai true jika dan hanya jika FlightLayer merupakan elemen dari sourcemap dan sebaliknya bernilai false jika dan hanya jika FlightLayer merupakan elemen dari overviewmap
         */
        public void setFlights(List flights, boolean isMap) {              //mengupdate tampilan pada FlightLayer sesuai dengan data flight yang diterima
            this.isMap = isMap;

            this.flights = new ArrayList < Flight >();
            synchronized (flights) {
                Iterator flightsIter = flights.iterator();
                while (flightsIter.hasNext()) {
                    this.flights.add(new Flight((Flight)flightsIter.next()));
                }
            }
            
            //jika pesawat yang sudah tidak tertangkap radar memiliki catatan letak pelabelan, lakukan penghapusan
            List < String > temp = new ArrayList  < String >();
            Set < String > lastFlights = this.labels.keySet();
            for (String callSign : lastFlights) {
                boolean isExist = false;

                Iterator flightsIter = this.flights.iterator();
                while (flightsIter.hasNext() && !isExist) {
                    Flight flight = (Flight) flightsIter.next();
                    if (flight.getCallSign().equals(callSign)) {
                        isExist = true;
                    }
                }
                
                if (! isExist) {
                    if (this.labels.containsKey(callSign)) {
                        temp.add(callSign);
                    }
                }
            }

            for (String s : temp) {
                if (this.labels.containsKey(s)) {
                    this.labels.remove(s);
                }
            }

            //jika pesawat yang sudah tidak tertangkap radar memiliki catatan jalur yang ditempuh, lakukan penghapusan
            lastFlights = this.trails.keySet();
            for (String callSign : lastFlights) {
                boolean isExist = false;

                Iterator flightsIter = this.flights.iterator();
                while (flightsIter.hasNext() && !isExist) {
                    Flight flight = (Flight) flightsIter.next();
                    if (flight.getCallSign().equals(callSign)) {
                        isExist = true;
                    }
                }

                if (! isExist) {
                    if (this.trails.containsKey(callSign)) {
                        if (! this.trailed.containsKey(callSign)) {
                            this.trailed.put(callSign, System.currentTimeMillis());
                        }
                    }
                }
            }

            temp.clear();
            Set < String > detrail = this.trailed.keySet();
            for (String callSign : detrail) {
                if (System.currentTimeMillis() - this.trailed.get(callSign) > 60000) { //jika pesawat hilang dari radar selama lebih dari 5 menit, hapus jejaknya
                    temp.add(callSign);
                }
            }

            for (String s : temp) {
                if (this.trails.containsKey(s)) {
                    this.trails.remove(s);
                }
                if (this.trailed.containsKey(s)) {
                    this.trailed.remove(s);
                }
            }

            if ((this.flights != null) && (this.flights.isEmpty())) {
                this.trails.clear();
                this.labels.clear();
            }
            
            this.graphics.clear();

            Iterator flightsIter = this.flights.iterator();
            while (flightsIter.hasNext()) {
                Flight flight = (Flight) flightsIter.next();
                String callSign = flight.getCallSign();

                if (this.trailed.containsKey(callSign)) {
                    this.trailed.remove(callSign);
                }

                float lat = flight.getLatitude(), lon = flight.getLongitude();
                int COLOR = WHITE;
                if (this.selectedFlightCS != null) {
                    if (callSign.equals(this.selectedFlightCS)) {
                        COLOR = RED;
                    }
                }
                if (this.f1 != null) {
                    if (callSign.equals(this.f1)) {
                        COLOR = ORANGE;
                    }
                }
                if (this.f2 != null) {
                    if (callSign.equals(this.f2)) {
                        COLOR = ORANGE;
                    }
                }
                drawFlight(lat, lon, null, COLOR);                                          //menggambarkan pesawat

                drawHeading(lat, lon, flight.getTrackangle(), flight.getGroundSpeed());     //menggambarkan arah pesawat
                
                //menggambarkan label pesawat
                StringBuilder strBuildr = new StringBuilder();
                int nLine = 1;
                if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showCode) {
                    strBuildr.append(flight.getCode()).append("\n");
                    nLine++;
                }
                strBuildr.append(callSign).append("\n");
                if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLat) {
                    int deg = (int)Math.floor(lat);
                    float dec = (lat - deg) * 60;
                    int min = (int)Math.floor(dec);
                    float sec = (dec - min) * 60;
                    strBuildr.append(deg).append("° ").append(min).append("' ").append(sec).append("\"");
                    if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLon) {
                        strBuildr.append("   ");
                    } else {
                        strBuildr.append("\n");
                    }
                }
                if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLon) {
                    int deg = (int)Math.floor(lon);
                    float dec = (lon - deg) * 60;
                    int min = (int)Math.floor(dec);
                    float sec = (dec - min) * 60;
                    strBuildr.append(deg).append("° ").append(min).append("' ").append(sec).append("\"").append("\n");
                }
                if ((cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLat) || (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLon)) {
                    nLine++;
                }
                if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showSpeed) {
                    strBuildr.append(flight.getGroundSpeed()).append(" knot").append("\n");
                    nLine++;
                }
                if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showTrackangle) {
                    strBuildr.append(flight.getTrackangle()).append(" °").append("\n");
                    nLine++;
                }
                if (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showAltitude) {
                    strBuildr.append(flight.getAltitude()).append(" fl").append("\n");
                    nLine++;
                }

                //menggambarkan informasi pesawat
                if ((this.detailedFlightCS != null) && (this.detailedFlightCS.equals(callSign))) {
                    String info = (
                            "Code          : " + flight.getCode() + "\n" +
                            "Call Sign     : " + flight.getCallSign() + "\n" +
                            "Coutry        : " + flight.getCountry() + "\n" +
                            "Altitude      : " + flight.getAltitude() + "\n" +
                            "Ground Speed  : " + flight.getGroundSpeed() + "\n" +
                            "Trackangel    : " + flight.getTrackangle() + "\n" +
                            "Vertical Rate : " + flight.getVerticalrate() + "\n" +
                            "Latitude      : " + flight.getLatitude() + "\n" +
                            "Longitude     : " + flight.getLongitude() + "\n" +
                            "Time of Day   : " + flight.getTimeOfDay() + "\n" +
                            "Last Update   : " + flight.getLastUpdate()
                            );
                    if (! this.labels.containsKey(callSign)) {                  //jika pesawat tidak memiliki catatan letak pelabelan, letakkan tepat di sebelah pesawat
                        drawFlightInfo(lat, lon, info);
                    } else {                                                    //jika pesawat memiliki catatan letak pelabelan, letakkan sesuai letak pada catatan
                        float llat = lat - this.labels.get(callSign).getLatitude();
                        float llon = lon - this.labels.get(callSign).getLongitude();
                        drawFlightInfo(llat, llon, info);
                        if ((lat != llat) || (lon != llon)) {
                            drawFlightLine(lat, lon, llat, llon);
                        }
                    }
                } else {
                    if (! this.labels.containsKey(callSign)) {                  //jika pesawat tidak memiliki catatan letak pelabelan, letakkan tepat di sebelah pesawat
                        drawFlightLabel(lat, lon, strBuildr.toString(), nLine, true);
                    } else {                                                    //jika pesawat memiliki catatan letak pelabelan, letakkan sesuai letak pada catatan
                        float llat = lat - this.labels.get(callSign).getLatitude();
                        float llon = lon - this.labels.get(callSign).getLongitude();
                        drawFlightLabel(llat, llon, strBuildr.toString(), nLine, false);
                        if ((lat != llat) || (lon != llon)) {
                            drawFlightLine(lat, lon, llat, llon);
                        }
                    }
                }
                
                //mengupdate catatan jalur yang ditempuh
                if (this.trails.containsKey(callSign)) {
                    ArrayList < LatLonPoint > trackHist = this.trails.get(callSign);
                    if ((trackHist.get(trackHist.size() - 1).getLatitude() != lat) || (trackHist.get(trackHist.size() - 1).getLongitude() != lon)) {
                        trackHist.add(new LatLonPoint(lat, lon));
                        this.trails.put(callSign, trackHist);
                    }
                } else {
                    ArrayList < LatLonPoint > trackHist = new ArrayList < LatLonPoint >();
                    trackHist.add(new LatLonPoint(lat, lon));
                    this.trails.put(callSign, trackHist);
                }
            }

            //menggambarkan jarak antara dua buah pesawat terpilih
            if ((this.f1 != null) && (this.f2 != null)) {
                Flight flight1 = this.getFlight(this.f1), flight2 = this.getFlight(this.f2);
                if ((flight1 != null) && (flight2 != null)) {
                    float lat0 = flight1.getLatitude(), lon0 = flight1.getLongitude(), latt = flight2.getLatitude(), lont = flight2.getLongitude();
                    drawDistanceLabel(lat0, lon0, latt, lont);
                }
            }

            //menggambarkan jarak dan sudut antara pesawat terpilih dengan pusat radar
            if ((this.BearingDistance) && (this.selectedFlightCS != null)) {
                String[] radarCenterLat = cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.latRadar;
                String[] radarCenterLon = cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.lonRadar;
                Flight selectedFlight = this.getFlight(this.selectedFlightCS);
                if (selectedFlight != null) {
                    float lat0 = selectedFlight.getLatitude(), lon0 = selectedFlight.getLongitude();
                    float latt = (radarCenterLat[3].charAt(0) == 'N' ? 1.0f : -1.0f) * (Integer.parseInt(radarCenterLat[0]) + (Integer.parseInt(radarCenterLat[1]) / 60.0f) + (Float.parseFloat(radarCenterLat[2]) / 3600.0f));
                    float lont = (radarCenterLon[3].charAt(0) == 'E' ? 1.0f : -1.0f) * (Integer.parseInt(radarCenterLon[0]) + (Integer.parseInt(radarCenterLon[1]) / 60.0f) + (Float.parseFloat(radarCenterLon[2]) / 3600.0f));
                    drawBearingDistanceLabel(lat0, lon0, latt, lont);
                }
            }

            drawTrail();                                                        //menggambarkan jalur yang ditempuh
            drawFlightPlan();                                                   //menggambarkan jalur flight plan

            Projection proj = this.map.getProjection();
            this.graphics.project(proj, true);
            repaint();

            if ((this.panel != null) && (this.isMap)) {
                //membangkitkan elevation view
                this.panel.removeAll();
                this.panel.setLayout(new BorderLayout());
                this.panel.add(this.layer.drawChart(flights, new LatLonPoint(proj.getUpperLeft().getLatitude(), proj.getUpperLeft().getLongitude() + ((this.map.getScale() / 10000000) * 3.575)), new LatLonPoint(proj.getLowerRight().getLatitude(), proj.getLowerRight().getLongitude() - ((this.map.getScale() / 10000000) * 1.85))), BorderLayout.CENTER);
                this.panel.validate();
            }
        }
        /**
         * menentukan opsi perhitungan jarak berada dalam mode menyala jika dan hanya jika calculateDistance bernilai true
         * @param calculateDistance boolean : bernilai true jika dan hanya jika mode penggambaran berada dalam perhitungan jarak antara dua buah pesawat dan berlaku false untuk sebaliknya
         */
        public void setCalculateDistance(boolean calculateDistance) {          //menentukan opsi perhitungan jarak berada dalam mode menyala jika dan hanya jika calculateDistance bernilai true
            /* I.S. sebarang */
            /* F.S. this.calculateDistance terdefinisi dan bernilai sama dengan parameter calculateDistance */
            /* Proses : mengisikan dan mengubah nilai this.calculateDistance dengan parameter calculateDistance */
                this.f1 = this.f2 = null;
                this.calculateDistance = calculateDistance;
        }
        /**
         * menentukan opsi perhitungan jarak dan sudut pesawat terpilih terhadap pusat radar berada dalam mode menyala jika dan hanya jika BearingDistance bernilai true
         * @param BearingDistance boolean : bernilai true jika dan hanya jika mode penggambaran berada dalam perhitungan jarak dan sudut pesawat terhadap pusat radar dan berlaku false untuk sebaliknya
         */
        public void setBearingDistance(boolean BearingDistance) {            //menentukan opsi perhitungan jarak dan sudut pesawat terpilih terhadap pusat radar berada dalam mode menyala jika dan hanya jika BearingDistance bernilai true
            /* I.S. sebarang */
            /* F.S. this.BearingDistance terdefinisi dan bernilai sama dengan parameter BearingDistance */
            /* Proses : mengisikan dan mengubah nilai this.BearingDistance dengan parameter BearingDistance */
                this.BearingDistance = BearingDistance;
        }
        /**
         * menentukan opsi penggambaran jalur yang ditempuh sesuai dengan const yang ada
         * @param trailMode int {NO_TRAIL, SELECTED_TRAIL, ALL_TRAIL}
         */
        public void setTrailMode(int trailMode) {                              //menentukan opsi penggambaran jalur yang ditempuh sesuai dengan const yang ada
            /* I.S. sebarang */
            /* F.S. this.trailMode terdefinisi dan bernilai sama dengan parameter trailMode */
            /* Proses : mengisikan dan mengubah nilai this.trailMode dengan parameter trailMode */
                this.trailMode = trailMode;
        }
        /**
         * menentukan opsi penggambaran flight plan sesuai dengan const yang ada
         * @param flightplanMode int {NO_FLIGHTPLAN, SELECTED_FLIGHTPLAN, ALL_FLIGHTPLAN}
         */
        public void setFlightPlanMode(int flightplanMode) {                    //menentukan opsi penggambaran flight plan sesuai dengan const yang ada
            /* I.S. sebarang */
            /* F.S. this.flightplanMode terdefinisi dan bernilai sama dengan parameter flightplanMode */
            /* Proses : mengisikan dan mengubah nilai this.flightplanMode dengan parameter flightplanMode */
                this.flightplanMode = flightplanMode;
        }

    /* getter */
        private Flight getFlight (float lat, float lon, float limit) {
            /* mengembalikan nilai dari atribut flights yang mana berada pada area lat - limit, lat + limit, lon - limit, lon + limit dan paling mendekati lat, lon atau null jika tidak ada */
            if (this.flights == null) {
                return (null);
            }

            Flight RetVal = null;
            float delta = limit;
            
            Iterator flightsIter = this.flights.iterator();
            
            while (flightsIter.hasNext()) {
                Flight flight = (Flight) flightsIter.next();
                float dlat = Math.abs(flight.getLatitude() - lat), dlon = Math.abs(flight.getLongitude() - lon);
                if (dlat < delta) {
                    delta = dlat;
                    RetVal = flight;
                }
                if (dlon < delta) {
                    delta = dlon;
                    RetVal = flight;
                }
            }

            if (RetVal != null) {
                Projection projctn = this.map.getProjection();
                Point fp = projctn.forward(RetVal.getLatitude(), RetVal.getLongitude());
                Point mep = projctn.forward(lat, lon);
                if (! ((Math.abs(fp.getX() - mep.getX()) <= (FLIGHT_RADIUS / 2)) && (Math.abs(fp.getY() - mep.getY()) <= (FLIGHT_RADIUS / 2)))) {
                    RetVal = null;
                }
            }

            return (RetVal);
        }
        private Flight getFlight (String callSign) {
            /* mengembalikan nilai dari atribut flights yang mana callSign = param */
            if (this.flights == null) {
                return (null);
            }

            Iterator flightsIter = this.flights.iterator();

            while (flightsIter.hasNext()) {
                Flight flight = (Flight) flightsIter.next();
                if (flight.getCallSign().equals(callSign)) {
                    return (flight);
                }
            }

            return (null);
        }
    
    /* method - method yang dimiliki oleh kelas FlightLayer */
        /**
         * melakukan perhitungan jarak antara dua buah pesawat terpilih menggunakan formula haversine
         * @param lat0 float yang merupakan posisi dari pesawat pertama dalam koordinat lintang
         * @param lon0 float yang merupakan posisi dari pesawat pertama dalam koordinat bujur
         * @param latt float yang merupakan posisi dari pesawat lainnya dalam koordinat lintang
         * @param lont float yang merupakan posisi dari pesawat lainnya dalam koordinat bujur
         * @return double yang merupakan jarak antara kedua pesawat dalam km
         */
        public static double CalcDistance(double lat0, double lon0, double latt, double lont) {    //melakukan perhitungan jarak antara dua buah pesawat terpilih
            double dlat = (latt - lat0) * GlobalEnvVars.RADIANS_PER_DEGREE, dlon = (lont - lon0) * GlobalEnvVars.RADIANS_PER_DEGREE;
            double a = Math.pow(Math.sin(dlat / 2.0f), 2.0f) + (Math.cos(lat0 * GlobalEnvVars.RADIANS_PER_DEGREE) * Math.cos(latt * GlobalEnvVars.RADIANS_PER_DEGREE) * Math.pow(Math.sin(dlon / 2.0f), 2.0f));
            double c = 2.0f * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
            return (c * GlobalEnvVars.EARTH_RADIUS_KM);
        }

        private void drawFlight (float lat, float lon, String label, int COLOR) {           //menggambarkan pesawat
            Color color = COLORS[COLOR];
            OMLabelCircle point = new OMLabelCircle(lat, lon, FLIGHT_RADIUS, label, color, true);
            this.graphics.addOMGraphic(point);
        }

        private void drawHeading (float lat, float lon, float angle, float speed) {         //menggambarkan arah pesawat
            Color color = COLORS[WHITE];

            float distance = 25;
            int xChange = (int)((float)Math.cos((angle - 90) * GlobalEnvVars.RADIANS_PER_DEGREE) * distance);
            int yChange = (int)((float)Math.sin((angle - 90) * GlobalEnvVars.RADIANS_PER_DEGREE) * distance);
            Point p = this.map.getProjection().forward(lat, lon);
            
            OMLine line = new OMLine(p.x, p.y, (p.x + xChange), (p.y + yChange));
            line.addArrowHead(OMArrowHead.ARROWHEAD_DIRECTION_FORWARD, 100, 3, 5);
            line.setLinePaint(color);
            this.graphics.addOMGraphic(line);
        }

        private void drawTrail () {                                             //menggambarkan jalur yang ditempuh pesawat
            if (this.trailMode == ALL_TRAIL) {
                for (Map.Entry < String, ArrayList < LatLonPoint > > trail : this.trails.entrySet()) {
                    Color color = trail.getKey().equals(this.selectedFlightCS) ? COLORS[RED] : (this.trailed.containsKey(trail.getKey()) ? COLORS[GRAY] : COLORS[WHITE]);
                    for (LatLonPoint point : trail.getValue()) {
                        OMPoint p = new OMPoint(point.getLatitude(), point.getLongitude(), 1);
                        p.setLinePaint(color);
                        p.setFillPaint(color);
                        this.graphics.addOMGraphic(p);
                    }
                }
            } else if (this.trailMode == SELECTED_TRAIL) {
                Color color = COLORS[RED];
                if (this.trails.containsKey(this.selectedFlightCS)) {
                    for (LatLonPoint point : this.trails.get(this.selectedFlightCS)) {
                        OMPoint p = new OMPoint(point.getLatitude(), point.getLongitude(), 1);
                        p.setLinePaint(color);
                        p.setFillPaint(color);
                        this.graphics.addOMGraphic(p);
                    }
                }
            }
        }

        private void drawFlightPlan () {                                        //menggambarkan flightplan pesawat
            if (this.flightplanMode == ALL_FLIGHTPLAN) {
                List < FlightPlan > listAllFlightPlan = this.FlightPlanList.getFlightPlanList();
                if (listAllFlightPlan != null) {
                    Color color = COLORS[ORANGE];
                    Iterator < FlightPlan > flightplanIter = listAllFlightPlan.iterator();
                    while (flightplanIter.hasNext()) {
                        FlightPlan flightplan = flightplanIter.next();
                        List < Fix > fixesList = (List < Fix >) flightplan.fixesList();
                        float LatLonPoints[] = new float[fixesList.size() * 2];
                        for (int i = 0; i < fixesList.size(); ++i) {
                            LatLonPoints[(i * 2)] = (float)fixesList.get(i).getLatitude();
                            LatLonPoints[((i * 2) + 1)] = (float)fixesList.get(i).getLongitude();
                        }
                        OMPoly polygon = new OMPoly(LatLonPoints, OMGraphicConstants.DECIMAL_DEGREES, OMGraphicConstants.LINETYPE_STRAIGHT);
                        polygon.setLinePaint(color);
                        graphics.addOMGraphic(polygon);
                    }
                }
            } else if (this.flightplanMode == SELECTED_FLIGHTPLAN) {
                if (this.selectedFlightCS != null) {
                    FlightPlan selectedFlightPlan = this.FlightPlanList.getFlightPlan(this.selectedFlightCS);
                    if (selectedFlightPlan != null) {
                        Color color = COLORS[RED];
                        List < Fix > fixesList = (List < Fix >) selectedFlightPlan.fixesList();
                        float LatLonPoints[] = new float[fixesList.size() * 2];
                        for (int i = 0; i < fixesList.size(); ++i) {
                            LatLonPoints[(i * 2)] = (float)fixesList.get(i).getLatitude();
                            LatLonPoints[((i * 2) + 1)] = (float)fixesList.get(i).getLongitude();
                        }
                        OMPoly polygon = new OMPoly(LatLonPoints, OMGraphicConstants.DECIMAL_DEGREES, OMGraphicConstants.LINETYPE_STRAIGHT);
                        polygon.setLinePaint(color);
                        graphics.addOMGraphic(polygon);
                    }
                }
            }
        }

        private void drawFlightLabel (float lat, float lon, String label, int nLine, boolean offset) {      //menggambarkan label pesawat
            Color color = COLORS[WHITE];
            int offX = offset ? -8 : 0, offY = (offset ? -8 : 0) - (15 * nLine);
            OMText text = new OMText(lat, lon, offX, offY, label, OMText.JUSTIFY_CENTER);
            text.setLinePaint(color);
            this.graphics.addOMGraphic(text);
        }

        private void drawFlightLine (float lat0, float lon0, float latt, float lont) {      //menggambarkan garis yang menghubungkan pesawat dengan labelnya
            Color color = COLORS[YELLOW];
            OMLine line = new OMLine(lat0, lon0, latt, lont, OMLine.STRAIGHT_LINE);
            line.setLinePaint(color);
            this.graphics.addOMGraphic(line);
        }

        private void drawFlightInfo (float lat, float lon, String info) {       //menggambarkan informasi dari pesawat terpilih
            Color bgcolor = COLORS[BLACK], fgcolor = COLORS[ORANGE];
            
            int offX = 25, offY = 25;
            OMText text = new OMText(lat, lon, offX, offY, info, OMText.JUSTIFY_LEFT);
            text.setFont(new Font("monospaced", Font.PLAIN, 12));
            text.setLinePaint(fgcolor);
            this.graphics.addOMGraphic(text);
            
            OMRect box = new OMRect(lat, lon, 0, 0, 225, getFontMetrics(getFont()).getHeight() * 15);
            box.setFillPaint(bgcolor);
            box.setLinePaint(fgcolor);
            this.graphics.addOMGraphic(box);
        }

        private void drawDistanceLabel (float lat0, float lon0, float latt, float lont) {   //menggambarkan label yang menampilkan informasi jarak antara dua buah pesawat terpilih
            Color color = COLORS[ORANGE];

            OMLine line = new OMLine(lat0, lon0, latt, lont, OMLine.STRAIGHT_LINE);
            line.setLinePaint(color);
            this.graphics.addOMGraphic(line);

            float d = (float)CalcDistance(lat0, lon0, latt, lont);
            int offX = -8, offY = -8;
            
            OMText text = new OMText((latt - ((latt - lat0) / 2.0f)), (lont - ((lont - lon0) / 2.0f)), offX, offY, "" + d + " km", OMText.JUSTIFY_CENTER);
            text.setLinePaint(color);
            this.graphics.addOMGraphic(text);
        }

        private void drawBearingDistanceLabel (float lat0, float lon0, float latt, float lont) {
            //menggambarkan label yang menampilkan informasi jarak dan sudut antara pesawat terpilih dengan titik pusat radar
            Color color = COLORS[RED];

            OMLine line = new OMLine(lat0, lon0, latt, lont, OMLine.STRAIGHT_LINE);
            line.setLinePaint(color);
            this.graphics.addOMGraphic(line);

            float d = (float)CalcDistance(lat0, lon0, latt, lont) / 1.852f;
            /*
            Point p0 = this.projection.forward(latt, lont), p1 = this.projection.forward(lat0, lon0);
            int dx = Math.abs(p1.x - p0.x) * (lon0 > lont ? 1 : -1), dy = Math.abs(p1.y - p0.y) * (lat0 > latt ? 1 : -1);
            float deg;
            
            if (dx == 0) {          //sejajar sumbu - y
                if (dy >= 0) {
                    deg = 0.0f;
                } else {
                    deg = 180.0f;
                }
            } else if (dy == 0) {   //sejajar sumbu - x
                if (dx > 0) {
                    deg = 90.0f;
                } else {
                    deg = 270.0f;
                }
            } else {
                float m = (float)dy / (float)dx;
                deg = (float)(Math.atan(m) / GlobalEnvVars.RADIANS_PER_DEGREE);
                if ((dx > 0) && (dy > 0)) {
                    deg = 90.0f - deg;
                } else if ((dx > 0) && (dy < 0)) {
                    deg = 90.0f + 360.0f - deg;
                } else if ((dx < 0) && (dy < 0)) {
                    deg = 90.0f + 360.0f + deg;
                } else {
                    deg = -90.0f + 360.0f - deg;
                }
            }

            while (deg >= 360.0f) {
                deg -= 360.0f;
            }

            while (deg < 0.0f) {
                deg += 360.0f;
            }
            */

            float deg = (float)(Math.atan2((Math.sin(lon0 - lont) * Math.cos(lat0)), ((Math.cos(latt) * Math.sin(lat0)) - (Math.sin(latt) * Math.cos(lat0) * Math.cos(lon0 - lont)))) / GlobalEnvVars.RADIANS_PER_DEGREE);
            
            int offX = -8, offY = -8;

            OMText text = new OMText((latt - ((latt - lat0) / 2.0f)), (lont - ((lont - lon0) / 2.0f)), offX, offY, "" + (deg < 0.0f ? (deg + 360.0f) : deg) + "°        " + d + " nm", OMText.JUSTIFY_CENTER);
            text.setLinePaint(color);
            this.graphics.addOMGraphic(text);
        }

        private boolean isLabelClicked (float lat, float lon) {                 //mengembalikan true jika dan hanya jika event click terjadi di daerah sekitar label dari pesawat terpilih dan demikian pula berlaku sebaliknya
            if (this.selectedFlightCS != null) {
                Flight flight = this.getFlight(this.selectedFlightCS);
                if (flight != null) {
                    float flat = flight.getLatitude(), flon = flight.getLongitude();
                    
                    Point plat = this.map.getProjection().forward(lat, lon), pflat = this.map.getProjection().forward(flat, flon);

                    boolean xlabel = (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLat || cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLon) ? true : false;
                    int ylabel =    (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showCode ? 1 : 0) +
                                    ((cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLat || cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showLon) ? 1 : 0) +
                                    (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showSpeed ? 1 : 0) +
                                    (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showTrackangle ? 1 : 0) +
                                    (cnsatmsmart.automation.mmi.helper.utilities.GlobalEnvVars.showAltitude ? 1 : 0);
                    
                    if (this.labels.containsKey(this.selectedFlightCS)) {
                        LatLonPoint pd = this.labels.get(this.selectedFlightCS);
                        float llat = flat - pd.getLatitude(), llon = flon - pd.getLongitude();

                        Point pllat = this.map.getProjection().forward(llat, llon);
                        
                        if ((this.detailedFlightCS != null) && (this.detailedFlightCS.equals(this.selectedFlightCS))) {
                            int dxl = Math.abs(pllat.x - plat.x) + (lon > llon ? 0 : 226), dyl = Math.abs(pllat.y - plat.y) + (lat < llat ? 0 : 326);
                            if ((dxl <= 225) && (dyl <= 325)) {
                                return (true);
                            } else {
                                int dxf = Math.abs(pflat.x - plat.x), dyf = Math.abs(pflat.y - plat.y);
                                if ((dxf <= (FLIGHT_RADIUS / 2)) && (dyf <= (FLIGHT_RADIUS / 2))) {
                                    return (true);
                                } else {
                                    return (false);
                                }
                            }
                        } else {
                            int dxl = Math.abs(pllat.x - plat.x), dyl = Math.abs(pllat.y - plat.y) + (lat > llat ? 0 : 114);
                            if ((dxl <= (xlabel ? 115 : 60)) && (dyl <= (33 + (ylabel * 16)))) {
                                return (true);
                            } else {
                                int dxf = Math.abs(pflat.x - plat.x), dyf = Math.abs(pflat.y - plat.y);
                                if ((dxf <= (FLIGHT_RADIUS / 2)) && (dyf <= (FLIGHT_RADIUS / 2))) {
                                    return (true);
                                } else {
                                    return (false);
                                }
                            }
                        }
                    } else {
                        if ((this.detailedFlightCS != null) && (this.detailedFlightCS.equals(this.selectedFlightCS))) {
                            int dxf = Math.abs(pflat.x - plat.x) + (lon > flon ? 0 : 226), dyf = Math.abs(pflat.y - plat.y) + (lat < flat ? 0 : 326);
                            if ((dxf <= 225) && (dyf <= 325)) {
                                return (true);
                            } else {
                                return (false);
                            }
                        } else {
                            int dxf = Math.abs(pflat.x - plat.x), dyf = Math.abs(pflat.y - plat.y) + (lat > flat ? 0 : 114);
                            if ((dxf <= (xlabel ? 115 : 60)) && (dyf <= (33 + (ylabel * 16)))) {
                                return (true);
                            } else {
                                return (false);
                            }
                        }

                        
                    }
                } else {
                    return (false);
                }
            } else {
                return (false);
            }
        }

    /**
     * event handler untuk event perubahan projection
     * @param pe ProjectionEvent event projection yang terjadi
     */
    public void projectionChanged(ProjectionEvent pe) {
        this.projection = (Projection) pe.getProjection().makeClone();
        this.graphics.project(this.projection, true);
        repaint();
    }

    @Override
    public void paint (Graphics g) {
        this.graphics.render(g);
    }

    /**
     * mengembalikan gestur dari tetikus
     * @return String[] yang merupakan senarai berisi ID dari mode tetikus
     */
    public String[] getMouseModeServiceList() {
        String[] RetVal = new String[1];
        RetVal[0] = SelectMouseMode.modeID; //gestures
        return (RetVal);
    }

    /**
     * melakukan pencatatan posisi kursor pada layar ketika mouse ditekan
     * @param me MouseEvent
     * @return boolean TRUE
     */
    public boolean mousePressed(MouseEvent me) {                                //melakukan pencatatan posisi kursor pada layar ketika mouse ditekan
        if (this.isMap) {
            this.mouseClicked = me;
        }
        return (true);
    }

    /**
     * melakukan pencatatan posisi kursor pada layar ketika mouse dilepaskan
     * @param me MouseEvent
     * @return boolean : bernilai true jika dan hanya jika dragging dilakukan pada label dari pesawat terpilih
     */
    public boolean mouseReleased(MouseEvent me) {                               //melakukan pencatatan posisi kursor pada layar ketika mouse dilepaskan
        if (this.isMap) {
            if ((this.mouseClicked != null) && (! me.getPoint().equals(this.mouseClicked.getPoint()))) {    //jika melakukan dragging
                //pindahkan letak label pesawat terpilih ke tempat yang diinginkan
                if (this.mouseClicked instanceof MapMouseEvent) {
                    MapMouseEvent mmeC = (MapMouseEvent) this.mouseClicked;
                    if (isLabelClicked(mmeC.getLatLon().getLatitude(), mmeC.getLatLon().getLongitude())) {
                        if (me instanceof MapMouseEvent) {
                            MapMouseEvent mmeR = (MapMouseEvent) me;
                            Flight flight = this.getFlight(this.selectedFlightCS);
                            if (flight != null) {
                                LatLonPoint LatLon = mmeR.getLatLon();
                                if ((LatLon.getLatitude() != flight.getLatitude()) || (LatLon.getLongitude() != flight.getLongitude())) {
                                    this.labels.put(this.selectedFlightCS, new LatLonPoint(flight.getLatitude() - LatLon.getLatitude(), flight.getLongitude() - LatLon.getLongitude()));
                                } else {
                                    if (this.labels.containsKey(this.selectedFlightCS)) {
                                        this.labels.remove(this.selectedFlightCS);
                                    }
                                }
                            }
                        }
                        return (true);
                    }
                }
            }
        }
    
        return (false);
    }

    /**
     * event handler untuk event mouse click
     * @param me MouseEvent
     * @return boolean : bernilai true jika dan hanya jika event yang terjadi adalah event yang dapat dikenali, yakni :<br />
     * single left click dengan opsi perhitungan jarak dalam mode menyala -> untuk menghitung jarak dari dua buah pesawat<br />
     * single left click dengan opsi perhitungan jarak dalam mode mati -> untuk memilih pesawat terdekat dengan toleransi tertentu<br />
     * single middle click -> untuk membatalkan pesawat terpilih<br />
     * single right click -> untuk menampilkan informasi dari pesawat<br />
     * double left click -> untuk mengembalikan label pesawat ke posisi semula<br />
     */
    public boolean mouseClicked(MouseEvent me) {
        if (this.isMap) {
            switch (me.getClickCount()) {
                case 1 :                                                            //single-click
                    if (me.getButton() == 1) {                                      //left-click
                        if (me instanceof MapMouseEvent) {
                            MapMouseEvent mme = (MapMouseEvent) me;
                            if (this.calculateDistance) {                           //opsi perhitungan jarak dalam mode menyala
                                //tentukan pesawat terpilih
                                Flight f = this.getFlight(mme.getLatLon().getLatitude(), mme.getLatLon().getLongitude(), 0.5f);
                                if (f != null) {
                                    if (((f1 == null) && (f2 == null)) || ((f1 != null) && (f2 != null))) {
                                        this.f1 = f.getCallSign();
                                        this.f2 = null;
                                    } else {
                                        if (! f.getCallSign().equals(this.f1)) {
                                            this.f2 = f.getCallSign();
                                        }
                                    }
                                }
                            } else {                                                //opsi perhitungan jarak dalam mode mati
                                //tentukan pesawat terpilih
                                Flight selectedFlight = this.getFlight(mme.getLatLon().getLatitude(), mme.getLatLon().getLongitude(), 0.5f);
                                if (selectedFlight != null) {
                                    if (this.selectedFlightCS != null) {
                                        if (! this.selectedFlightCS.equals(selectedFlight.getCallSign())) {
                                            this.selectedFlightCS = selectedFlight.getCallSign();
                                        }
                                    } else {
                                        this.selectedFlightCS = selectedFlight.getCallSign();
                                    }
                                }
                            }
                            return (true);
                        } else {
                            return (false);
                        }
                    } else if (me.getButton() == 2) {                               //middle-click
                        //batalkan pesaawat tepilih
                        if (this.calculateDistance) {
                            this.f1 = null;
                            this.f2 = null;
                        } else {
                            this.selectedFlightCS = null;
                        }
                        return (true);
                    } else if (me.getButton() == 3) {                               //right-click
                        //tampilkan informasi pesawat
                        if (me instanceof MapMouseEvent) {
                            MapMouseEvent mme = (MapMouseEvent) me;
                            Flight detailedFlight = this.getFlight(mme.getLatLon().getLatitude(), mme.getLatLon().getLongitude(), 0.5f);
                            if (detailedFlight != null) {
                                if (this.detailedFlightCS != null) {
                                    if (! this.detailedFlightCS.equals(detailedFlight.getCallSign())) {
                                        this.detailedFlightCS = detailedFlight.getCallSign();
                                    } else {
                                        this.detailedFlightCS = null;
                                    }
                                } else {
                                    this.detailedFlightCS = detailedFlight.getCallSign();
                                }
                            } else {
                                if (this.detailedFlightCS != null) {
                                    this.detailedFlightCS = null;
                                } else {
                                    this.PopUpMenu.show(me.getComponent(), me.getX(), me.getY());
                                }
                            }
                            return (true);
                        } else {
                            return (false);
                        }
                    } else {
                        return (false);
                    }
                case 2 :                                                            //double-click
                    if (me.getButton() == 1) {                                      //left-click
                        //kembalikan label pesawat ke posisi semula
                        if (me instanceof MapMouseEvent) {
                            MapMouseEvent mme = (MapMouseEvent) me;
                            if (isLabelClicked(mme.getLatLon().getLatitude(), mme.getLatLon().getLongitude())) {
                                if (this.labels.containsKey(this.selectedFlightCS)) {
                                    this.labels.remove(this.selectedFlightCS);
                                    return (true);
                                } else {
                                    return (false);
                                }
                            } else {
                                return (false);
                            }
                        } else {
                            return (false);
                        }
                    } else if (me.getButton() == 2) {
                        return (false);
                    } else if (me.getButton() == 3) {
                        return (false);
                    } else {
                        return (false);
                    }
                default :
                    return (false);
            }
        }

        return (false);
    }

    /**
     * event handler untuk kejadian dimana tetikus memasuki area FlightLayer
     * #do nothing
     * @param me MouseEvent
     */
    public void mouseEntered(MouseEvent me) { }

    /**
     * event handler untuk kejadian dimana tetikus meninggalkan area FlightLayer
     * #do nothing
     * @param me MouseEvent
     */
    public void mouseExited(MouseEvent me) { }

    /**
     * event handler untuk event mouse drag
     * @param me MouseEvent
     * @return boolean FALSE
     */
    public boolean mouseDragged(MouseEvent me) {
        return (false);
    }

    /**
     * event handler untuk event mouse move
     * @param me MouseEvent
     * @return boolean FALSE
     */
    public boolean mouseMoved(MouseEvent me) {
        return (false);
    }

    /**
     * event handler untuk event mouse move
     * #do nothing
     */
    public void mouseMoved() { }

    /**
     * mengembalikan MapMouseListener
     * @return MapMouseListener yang merupakan this
     */
    @Override
    public MapMouseListener getMapMouseListener () {
        return (this);
    }
}
